/* Copyright (c) 2008-2010, developers of the Ascension Log Visualizer
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom
 * the Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

package com.googlecode.logVisualizer.gui;

import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.util.*;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.swing.*;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.table.AbstractTableModel;

import net.java.dev.spellcast.utilities.UtilityConstants;
import net.java.swingfx.waitwithstyle.PerformanceInfiniteProgressPanel;

import org.jfree.ui.RefineryUtilities;

import com.googlecode.logVisualizer.Settings;
import com.googlecode.logVisualizer.parser.LogsCreator;

/**
 * A dialog to select mafia logs for direct visualisation without having to
 * first turn them into parsed ascension logs.
 * <p>
 * This class only gives controls to select mafia logs for visualisation, it
 * does not parse those logs itself. Such a task has to be handled by another
 * class. The selected condensed mafia logs are delegated from this class
 * through the {@link MafiaLogLoaderListener} instance which is needed for the
 * constructor.
 * <p>
 * Note that the mafia log delegation through the {@link MafiaLogLoaderListener}
 * interface must be able to handle concurrent method calls from this class.
 */
final class MafiaLogsVisualizerDialog extends JDialog {
    private static final FilenameFilter MAFIA_LOG_FILTER = new FilenameFilter() {
        private final Matcher mafiaLogMatcher = Pattern.compile(".*_\\d+\\.txt$").matcher("");

        private final String preparsedLogPartialFileString = "_ascend";

        public boolean accept(
                              final File dir, final String name) {
            return mafiaLogMatcher.reset(name).matches()
                   && !name.contains(preparsedLogPartialFileString);
        }
    };

    private static final List<File> EMPTY_MAFIA_LOGS_LIST = new ArrayList<File>(1);

    private final JTextField mafiaLogsDirectoryField;

    private final JTable visualizableMafiaLogsTable;

    private final JCheckBox toggleAllBox;

    private final MafiaLogLoaderListener mafiaLogLoaderListener;

    /**
     * Constructs the object.
     * <p>
     * Note that the mafia log delegation through the
     * {@link MafiaLogLoaderListener} interface must be able to handle
     * concurrent method calls from this class.
     * 
     * @param owner
     *            The owner of this dialog.
     * @param mafiaLogLoaderListener
     *            The interface through which the selected condensed mafia logs
     *            are delegated to another class.
     * @throws NullPointerException
     *             if owner is {@code null}; if mafiaLogLoaderListener is
     *             {@code null}
     */
    MafiaLogsVisualizerDialog(
                              final JFrame owner,
                              final MafiaLogLoaderListener mafiaLogLoaderListener) {
        super(owner, true);

        if (mafiaLogLoaderListener == null)
            throw new NullPointerException("mafiaLogLoaderListener must not be null.");

        setLayout(new BorderLayout(5, 10));
        setTitle("Mafia Logs Parser");
        setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE);
        setGlassPane(new PerformanceInfiniteProgressPanel());

        this.mafiaLogLoaderListener = mafiaLogLoaderListener;
        visualizableMafiaLogsTable = new JTable(new MafiaLogsTableModel(EMPTY_MAFIA_LOGS_LIST));
        toggleAllBox = new JCheckBox("Visualize all logs");
        toggleAllBox.addChangeListener(new ChangeListener() {
            public void stateChanged(
                                     final ChangeEvent e) {
                if (toggleAllBox.hasFocus())
                    ((MafiaLogsTableModel) visualizableMafiaLogsTable.getModel()).setVisualizeAll(toggleAllBox.isSelected());
            }
        });
        mafiaLogsDirectoryField = new JTextField(Settings.getSettingString("Mafia logs location"));
        mafiaLogsDirectoryField.addActionListener(new ActionListener() {
            public void actionPerformed(
                                        final ActionEvent e) {
                createMafiaLogsTable();
            }
        });
        createMafiaLogsTable();

        final JButton runButton = new JButton("Run parser");
        final JButton cancelButton = new JButton("Cancel");
        runButton.addActionListener(new ActionListener() {
            public void actionPerformed(
                                        final ActionEvent e) {
                if (((MafiaLogsTableModel) visualizableMafiaLogsTable.getModel()).isVisualizationsOccur()) {
                    setWaitingForComputationEnd(true);
                    runParser();
                } else
                    JOptionPane.showMessageDialog(null,
                                                  "There are no mafia logs selected to be visualized.",
                                                  "Nothing to visualize",
                                                  JOptionPane.INFORMATION_MESSAGE);
            }
        });
        cancelButton.addActionListener(new ActionListener() {

            public void actionPerformed(
                                        final ActionEvent e) {
                dispose();
            }
        });

        add(createMafiaLogsDirectoryFinderPanel(), BorderLayout.NORTH);

        add(createMafiaLogsTablePanel(), BorderLayout.CENTER);

        final JPanel buttonPanel = new JPanel(new GridLayout(1, 0, 10, 0));
        buttonPanel.setPreferredSize(new Dimension(150, 50));
        buttonPanel.add(runButton);
        buttonPanel.add(cancelButton);
        add(buttonPanel, BorderLayout.SOUTH);

        pack();
        final Dimension currentSize = getSize();
        if (currentSize.height < 500) {
            currentSize.height = 500;
            setSize(currentSize);
        } else if (currentSize.height > 700) {
            currentSize.height = 700;
            setSize(currentSize);
        }
        RefineryUtilities.centerFrameOnScreen(this);
        setVisible(true);
    }

    private JPanel createMafiaLogsDirectoryFinderPanel() {
        final JPanel panel = new JPanel(new GridBagLayout());
        panel.setBorder(BorderFactory.createTitledBorder("Mafia logs location"));

        final JButton directoryChooserButton = new JButton("Find Directory");
        GridBagConstraints gbc;

        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.insets = new Insets(5, 10, 5, 0);
        panel.add(mafiaLogsDirectoryField, gbc);

        gbc = new GridBagConstraints();
        gbc.gridx = 2;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.EAST;
        gbc.insets = new Insets(5, 25, 5, 10);
        panel.add(directoryChooserButton, gbc);

        File mafiaLogsDirectory = new File(Settings.getSettingString("Mafia logs location"));
        if (!mafiaLogsDirectory.exists())
            mafiaLogsDirectory = null;

        final JFileChooser directoryChooser = new JFileChooser(mafiaLogsDirectory);
        directoryChooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY);
        directoryChooserButton.addActionListener(new ActionListener() {

            public void actionPerformed(
                                        final ActionEvent e) {
                final int state = directoryChooser.showOpenDialog(null);
                if (state == JFileChooser.APPROVE_OPTION) {
                    mafiaLogsDirectoryField.setText(directoryChooser.getSelectedFile()
                                                                    .getAbsolutePath());
                    createMafiaLogsTable();
                }
            }
        });

        return panel;
    }

    private JPanel createMafiaLogsTablePanel() {
        final JPanel panel = new JPanel(new BorderLayout(5, 10));

        final JPanel bottomPanel = new JPanel(new GridBagLayout());
        GridBagConstraints gbc;

        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.insets = new Insets(0, 10, 5, 0);
        bottomPanel.add(toggleAllBox, gbc);

        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.anchor = GridBagConstraints.WEST;
        gbc.fill = GridBagConstraints.BOTH;
        gbc.weightx = 1.0;
        gbc.weighty = 1.0;
        gbc.insets = new Insets(10, 5, 5, 5);
        bottomPanel.add(new JLabel("<html>Note that depending on the number of mafia logs selected and the amount and contents in those logs"
                                   + "<p>the parsing process may take a while to finish."
                                   + "<p><p>Also, note that it is not advisable to have much more than 10 mafia log charts open at the same time.</html>"),
                        gbc);

        final JScrollPane scrollPane = new JScrollPane(visualizableMafiaLogsTable);
        scrollPane.setPreferredSize(new Dimension(300, 300));

        panel.add(scrollPane, BorderLayout.CENTER);
        panel.add(bottomPanel, BorderLayout.SOUTH);

        return panel;
    }

    private void createMafiaLogsTable() {
        final File mafiaLogsDirectory = new File(mafiaLogsDirectoryField.getText());
        if (!mafiaLogsDirectory.exists() || !mafiaLogsDirectory.isDirectory()) {
            JOptionPane.showMessageDialog(null,
                                          "Please only specify existing directories.",
                                          "Problem occurred",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        final File[] mafiaLogs = mafiaLogsDirectory.listFiles(MAFIA_LOG_FILTER);
        if (mafiaLogs.length == 0) {
            JOptionPane.showMessageDialog(null,
                                          "The specified directory does not contain any mafia logs.",
                                          "Problem occurred",
                                          JOptionPane.WARNING_MESSAGE);
            return;
        }

        // If the input seems to be correct, save the directory used.
        Settings.setSettingString("Mafia logs location", mafiaLogsDirectoryField.getText());

        // In case there are still some logs in the temporary data directory
        // delete all of its contents.
        for (final File f : UtilityConstants.TEMP_LOCATION.listFiles())
            if (!f.isDirectory())
                f.delete();

        // Start the actual computation.
        setWaitingForComputationEnd(true);
        final ExecutorService executor = Executors.newSingleThreadExecutor();
        executor.execute(new Runnable() {
            public void run() {
                try {
                    final File[] condensedMafiaLogs = LogsCreator.createCondensedMafiaLogs(mafiaLogs);
                    ((MafiaLogsTableModel) visualizableMafiaLogsTable.getModel()).setMafiaLogs(Arrays.asList(condensedMafiaLogs));
                    toggleAllBox.setSelected(false);
                } catch (final IOException e) {
                    e.printStackTrace();
                } finally {
                    setWaitingForComputationEnd(false);
                }
            }
        });
        executor.shutdown();
    }

    /**
     * @param isComputationNotDone
     *            A flag showing whether the computation has ended or not.
     */
    private void setWaitingForComputationEnd(
                                             final boolean isComputationNotDone) {
        getGlassPane().setVisible(isComputationNotDone);
    }

    private void runParser() {
        final Thread workerThread = new Thread(new Runnable() {
            public void run() {
                // 4 Threads per CPU should be a high enough number to not slow
                // the computation too much down by scheduler overhead while
                // still making use of threaded computing.
                final ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime()
                                                                                     .availableProcessors() * 4);

                for (final File f : ((MafiaLogsTableModel) visualizableMafiaLogsTable.getModel()).getVisualizableMafiaLogs())
                    executor.execute(new Runnable() {
                        public void run() {
                            mafiaLogLoaderListener.visualizeMafiaLog(f);
                        }
                    });

                // Wait for all threads to finish.
                executor.shutdown();
                try {
                    executor.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
                } catch (final InterruptedException e) {
                    e.printStackTrace();
                }

                dispose();
            }
        });
        workerThread.start();
    }

    @Override
    public void dispose() {
        // Delete any leftover temporary data.
        for (final File f : UtilityConstants.TEMP_LOCATION.listFiles())
            if (!f.isDirectory())
                f.delete();

        super.dispose();
    }

    /**
     * TableModel used by the JTable which handles the selection of mafia logs
     * which should be visualised.
     */
    private static final class MafiaLogsTableModel extends AbstractTableModel {
        private static final String[] columnNames = { "Mafia log", "Should be visualized?" };

        private List<File> mafiaLogs;

        private List<Boolean> visualizables;

        MafiaLogsTableModel(
                            final Collection<File> mafiaLogs) {
            this.mafiaLogs = new ArrayList<File>(mafiaLogs);
            visualizables = new ArrayList<Boolean>(mafiaLogs.size() + 1);
            for (int i = 0, j = mafiaLogs.size(); i < j; i++)
                visualizables.add(false);
        }

        @Override
        public Class<?> getColumnClass(
                                       final int columnIndex) {
            if (columnIndex == 1)
                return Boolean.class;

            return String.class;
        }

        @Override
        public boolean isCellEditable(
                                      final int rowIndex, final int columnIndex) {
            return columnIndex == 1;
        }

        @Override
        public void setValueAt(
                               final Object aValue, final int rowIndex, final int columnIndex) {
            visualizables.set(rowIndex, (Boolean) aValue);
        }

        @Override
        public String getColumnName(
                                    final int column) {
            return columnNames[column];
        }

        public int getColumnCount() {
            return columnNames.length;
        }

        public int getRowCount() {
            return mafiaLogs.size();
        }

        public Object getValueAt(
                                 final int rowIndex, final int columnIndex) {
            return columnIndex == 0 ? mafiaLogs.get(rowIndex).getName().replace(".txt", "")
                                   : visualizables.get(rowIndex);
        }

        /**
         * Sets the contents of this model to the given collection of mafia
         * logs.
         * 
         * @param mafiaLogs
         *            A collection of condensed mafia logs.
         */
        void setMafiaLogs(
                          final Collection<File> mafiaLogs) {
            this.mafiaLogs = new ArrayList<File>(mafiaLogs);
            visualizables = new ArrayList<Boolean>(mafiaLogs.size());
            for (int i = 0, j = mafiaLogs.size(); i < j; i++)
                visualizables.add(false);

            fireTableDataChanged();
        }

        /**
         * @return The mafia logs which should be visualised.
         */
        List<File> getVisualizableMafiaLogs() {
            final List<File> visualizableMafiaLogs = new ArrayList<File>(mafiaLogs.size());

            for (int i = 0, j = visualizables.size(); i < j; i++)
                if (visualizables.get(i))
                    visualizableMafiaLogs.add(mafiaLogs.get(i));

            return visualizableMafiaLogs;
        }

        /**
         * @param isVisualizeAll
         *            A flag showing whether all mafia logs inside this model
         *            should be visualised or not.
         */
        void setVisualizeAll(
                             final boolean isVisualizeAll) {
            Collections.fill(visualizables, isVisualizeAll);
            fireTableDataChanged();
        }

        /**
         * @return {@code true} in case at least one mafia log from inside this
         *         model should be visualised.
         */
        boolean isVisualizationsOccur() {
            return visualizables.size() > 0 ? visualizables.contains(true) : false;
        }
    }

    /**
     * Interface used to delegate the visualisation of mafia logs to the place
     * where it is actually handled (which is not inside the
     * {@link MafiaLogsVisualizerDialog} class).
     */
    public static interface MafiaLogLoaderListener {
        public void visualizeMafiaLog(
                                      final File mafiaLog);
    }
}
